#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author  : Leo.W
# @Date    : 2024/1/24 16:56
# @File    : 7-decorator.py
# @Software: PyCharm

"""
装饰函数(装饰器)：为已经存在的函数或对象添加额外的功能。
装饰函数(装饰器)类似于Java中的注解，.NET 中的特性
在做面向对象编程时，经常会用到 @staticmethod 和 @classmethod 两个内置装饰器。
装饰器本质上是一个Python函数，它可以让其他函数在不需要做任何代码变动的前提下【增加额外功能】，装饰器的返回值也是一个函数对象。
它经常用于有切面需求的场景，比如：插入日志、性能测试、事务处理、缓存、权限校验等场景。
装饰器是解决这类问题的绝佳设计，有了装饰器，我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

参考：
https://www.cnblogs.com/tobyqin/p/python-decorator.html
https://zhuanlan.zhihu.com/p/640193185

在理解这些装饰器之前，最好对函数的闭包和装饰器的接口约定有一定了解，参见Python的闭包：
https://www.cnblogs.com/tobyqin/p/python-closure.html
"""

from functools import wraps


# ------------ 不带参数的装饰器 ------------
def decorator1(func):
    """
    自定义装饰器
    :param func: 被装饰的函数，即 @decorator1 装饰的函数
    :return:
    """

    def wrapper():  # 定义包装函数，用于在执行 func 之前/之后 执行自定义的业务逻辑
        print(f"wrapper of decorator1，func -> {func.__name__}")
        return func()  # 执行被装饰的函数，并返回结果

    return wrapper


# @ 符号是装饰器的语法糖，相当于：greet = decorator1(greet)
@decorator1
def greet():
    print("hello world")


greet()  # 执行函数，相当于：
# greet = decorator1(greet) # greet 其实是 wrapper
# greet()
# greet的__name__自然也就是wrapper了，不仅仅是name，其他属性也都是来自wrapper，比如doc，source等等
print(f"greet.__name__ -> {greet.__name__}")  # greet.__name__ -> wrapper
# 使用标准库里的functools.wraps，可以基本解决上述问题
# 如果要彻底解决这个问题可以借用第三方包，比如 wrapt


# ------------ 装饰带参数的函数 ------------
def decorator2(func):
    # 定义装饰器包装函数，使用 @wraps(func) 将被装饰的函数属性绑定到包装函数上
    @wraps(func)
    def wrapper(*args, **keywords):  # 指定与被装饰函数一毛一样的参数
        print("wrapper of decorator2")
        print("*args =>", args)
        print("*keywords =>")
        for kw in keywords:
            print(kw, ":", keywords[kw])
        return func(*args, **keywords)  # 执行被装饰的函数，并返回结果

    return wrapper


@decorator2
def say(*args, **kwargs):
    print("Hello ", args)


say("world")
say("Tom", ",", "have a nice day")
say("Jack", "!", "good luck!", day="Friday")
print(f"say.__name__ -> {say.__name__}")  # say.__name__ -> say


# ------------ 进阶：带参数的装饰器 ------------
def logging(level):
    """
    日志装饰器
    :param level: 日志级别
    :return: 装饰器函数
    """

    # 内部定义了一个装饰器函数并返回
    def decorator(func):
        # 内部装饰器包装函数
        @wraps(func)
        def wrapper(*args, **kwargs):  # 指定宇宙无敌参数,可以覆盖使用该装饰器函数所有参数场景
            print(f"logging->[{level}]: enter function {func.__name__}()")
            return func(*args, **kwargs)  # 执行被装饰的函数，并返回结果

        return wrapper

    return decorator


# 当带参数的装饰器被打在某个函数上时，比如@logging(level='DEBUG')，它其实是一个函数，会马上被执行，它返回的结果是一个装饰器：
# @ 语法糖等价于：
# wrapper = logging(level="INFO")
# say = wrapper(say)
@logging(level="INFO")
def say(something):
    print(f"say {something}!")


say("hello world")

# ------------ 进阶：基于类实现的装饰器 ------------
"""
装饰器函数其实是这样一个接口约束，它必须接受一个callable对象作为参数，然后返回一个callable对象。
在Python中一般callable对象都是函数，但也有例外。只要某个对象重载了内置方法__call__()，那么这个对象就是callable的。
class Test():
    def __call__(self):
        print 'call me!'
 
t = Test()
t()  # call me

装饰器要求接受一个callable对象，并返回一个callable对象（不太严谨）。那么用类来实现也是也可以的。我们可以让类的构造函数__init__()接受一个函数，然后重载__call__()并返回一个函数，也可以达到装饰器函数的效果。
"""


class Logging(object):
    def __init__(self, func):
        """
        构造函数：接受一个函数参数
        :param func: 被装饰函数
        """
        self.func = func

    def __call__(self, *args, **kwargs):
        """
        重载内置方法 __call__，相当装饰器的包装函数
        :param args: 接受来自被装饰函数的参数
        :param kwargs: 接受来自被装饰函数的参数
        :return: 被装饰函数的运行结果
        """
        print(f"Logging->[DEBUG]: enter function {self.func.__name__}()")
        return self.func(*args, **kwargs)  # 执行被装饰的函数，并返回结果


@Logging
def say1(something):
    print(f"say {something}!")


say1("hello")

"""
带参数的类装饰器:
构造函数__init__里接受的就不是一个函数，而是传入的参数。通过类把这些参数保存起来。
然后在重载__call__方法是就需要接受一个函数并返回一个函数。
"""


class Log(object):
    def __init__(self, level="INFO"):
        """
        构造函数
        :param level: 装饰器的参数
        """
        self.level = level

    def __call__(self, func):
        """
        重载内置方法 __call__
        :param func: 接受被装饰函数
        :return: 被装饰函数的运行结果
        """

        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"Logging->[{self.level}]: enter function {func.__name__}()")
            func(*args, **kwargs)  # 执行被装饰的函数，并返回结果

        return wrapper  # 装饰器


# @ 语法糖相当于
# log = Log(level="WARN")
# say2 = log.__call__(say2)
@Log(level="WARN")
def say2(something):
    print(f"say {something}!")


say2("hello")


# ------------ 进阶：内置的装饰器 ------------
"""
内置的装饰器和普通的装饰器原理是一样的，只不过返回的不是函数，而是类对象
三大内置装饰器： @property (属性装饰器)、@staticmethod （静态方法装饰器）、@classmethod （类方法装饰器）
@property 返回的是一个 property 类对象，
@staticmethod 返回的是一个 staticmethod 类对象，
@classmethod 返回的是一个 classmethod 类对象。

他们都是调用的是各自的__init__()构造函数。

@property 装饰器:用于装饰类的属性,被装饰的属性为【只读属性】

在了解这个装饰器前，先看看不使用装饰器怎么写一个属性：
def getx(self):
    return self._x
 
def setx(self, value):
    self._x = value
    
def delx(self):
   del self._x
 
# create a property
x = property(getx, setx, delx, "I am doc for x property")

以上就是一个Python属性的标准写法，其实和Java挺像的，但是太罗嗦。有了 @语法糖，能达到一样的效果但看起来更简单。

@property
def x(self):
    return self._x
"""


class User(object):
    def __init__(self, name):
        self._name = name

    # @property：定义只读属性。由于python进行属性的定义时，没办法设置私有属性，因此要通过@property的方法来进行设置。这样可以隐藏属性名，让用户进行使用的时候无法随意修改。
    @property
    def name(self):
        return self._name

    # 定义一个方法，用于修改 只读属性 name 值
    def set_name(self, n):
        self._name = n

    # @classmethod 返回的是一个 classmethod 类对象
    # 类方法的第一个参数 cls 表示当前类，而实例方法的第一个参数是self，表示该类的一个实例。
    # 有子类继承时，调用该类方法时，传入的类变量cls是子类，而非父类。
    # 对于类方法，可以通过类来调用(类似于Java类的静态方法)，如 User.to_string()。也可以通过类的一个实例来调用，就像User().to_string()
    @classmethod  # 等同于 to_string = classmethod(to_string)
    def to_string(cls):  # 类方法的第一个参数 cls 代表的是类，而非类的实例对象
        return "class {}".format(cls.__name__)

    @staticmethod  # 等同于 get_type = staticmethod(get_type)
    def get_type():  # 不需要表示自身对象的self和自身类的cls参数，就跟使用函数一样。
        return "class User"


u = User("admin")
# 注意：经过@property装饰过的函数返回的不再是一个函数，而是一个property对象。因此这里直接使用 u.name 不能加 ()
print(u.name)

# 直接使用 类.类方法() 调用 @classmethod 装饰的方法
print(User.to_string())  # class User
# 使用 类的实例对象.类方法() 调用，但是并没有什么用，违反了初衷：类方法就是专门供类使用
print(u.to_string())  # User {name : admin}

print(User.get_type())  # class User
print(u.get_type())  # class User

"""
既然@staticmethod和@classmethod都可以直接类名.方法名()来调用，那他们有什么区别呢:

@staticmethod 不需要表示自身对象的self和自身类的cls参数，就跟使用函数一样。
@classmethod 也不需要self参数，但第一个参数需要是表示自身类的cls参数。

如果在@staticmethod中要调用到这个类的一些属性方法，只能直接 类名.属性名 或 类名.方法名。
而@classmethod因为持有cls参数，可以来调用类的属性，类的方法，实例化对象等，避免硬编码。

注意：装饰器不能装饰 @staticmethod （静态方法） 或者 @classmethod （类方法）,因为这两个装饰器返回的是 对象 而非 方法
"""


class Car(object):
    def __init__(self, model):
        self.model = model

    @logging  # 装饰实例方法，OK
    def run(self):
        print(f"{self.model} is running!")

    # @staticmethod 这个装饰器，其实它返回的并不是一个callable对象，而是一个staticmethod对象，那么它是不符合装饰器要求的（比如传入一个callable对象）
    @logging(level="INFO")  # 装饰静态方法，Failed
    @staticmethod
    def check_model_for(obj):
        if isinstance(obj, Car):
            print(f"The model of your car is {obj.model}")
        else:
            print(f"{obj} is not a car!")

    # 把装饰器放在@staticmethod之前就好了,因为装饰器返回的还是一个正常的函数，然后再加上一个@staticmethod是不会出问题的。
    @staticmethod
    @logging(level="INFO")  # 在 @staticmethod 之前装饰，OK
    def start_engine():
        print("Ready to race !!!")


car = Car("F1")
car.start_engine()
Car.check_model_for("A1")  # A1 is not a car!


# -------------------------- 如何优化装饰器 --------------------------
"""
可以使用第三方包类改进装饰器:
decorator.py: 是一个非常简单的装饰器加强包。
- 可以很直观的先定义包装函数wrapper()，再使用decorate(func, wrapper)方法就可以完成一个装饰器。
from decorator import decorate
 
def wrapper(func, *args, **kwargs):
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
    return func(*args, **kwargs)
 
def logging(func):
    return decorate(func, wrapper)  # 用wrapper装饰func
    
- 也可以使用它自带的 @decorator 装饰器来完成你的装饰器。
@decorator
def logging(func, *args, **kwargs):
    print "[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__)
    return func(*args, **kwargs)
    
        
wrapt:
wrapt是一个功能非常完善的包，用于实现各种你想到或者你没想到的装饰器。
使用wrapt实现的装饰器你不需要担心之前inspect中遇到的所有问题，因为它都帮你处理了，甚至inspect.getsource(func)也准确无误。
http://wrapt.readthedocs.io/en/latest/quick-start.html
"""
import wrapt


# without argument in decorator
# 函数签名是固定的，必须是(wrapped, instance, args, kwargs)，注意第二个参数instance是必须的，
# args 和kwargs 也是固定的，注意前面没有星号
@wrapt.decorator
def logging2(wrapped, instance, args, kwargs):  # instance is must
    print("[DEBUG]: enter {}()".format(wrapped.__name__))
    return wrapped(*args, **kwargs)  # 注意：装饰器内部调用原函数时才带星号


@logging2
def say2(something):
    pass


# 使用wrapt写一个带参数的装饰器，可以这样写。
def logging3(level):
    # 返回一个 装饰器
    @wrapt.decorator
    def wrapper(wrapped, instance, args, kwargs):
        print("[{}]: enter {}()".format(level, wrapped.__name__))
        return wrapped(*args, **kwargs)

    return wrapper


@logging3(level="INFO")
def do(work):
    pass
